package jmine.tec.utils.spring;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import jmine.tec.utils.debug.impl.DebugRuntimeFactory;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.support.AbstractRefreshableApplicationContext;
import org.springframework.core.io.Resource;

import bancosys.tec.utils.md5sum.MD5SumTool;

/**
 * @author takeshi
 */
public class ApplicationContextRefresher implements ApplicationContextAware, DisposableBean, InitializingBean {

    private static final String[] EMPTY_STRING = new String[]{};

    /**
     * Task para verificar e fazer refresh no applicationContext
     * 
     * @author takeshi
     */
    private final class ApplicationContextRefresherTask implements Runnable {
        private final Resource[] resources;

        private long[] lastModified;

        private String[] md5;

        /**
         * C'tor
         * 
         * @param resources array de {@link Resource}
         * @param lastModified array com o {@link File#lastModified()}
         * @param md5 String o md5 ou uma string vazia
         */
        private ApplicationContextRefresherTask(Resource[] resources, long[] lastModified, String[] md5) {
            this.resources = resources;
            this.md5 = md5;
            this.lastModified = lastModified;
        }

        /**
         * {@inheritDoc}
         */
        public void run() {
            try {
                long[] modifications = lastModified(resources);
                if (!Arrays.equals(modifications, lastModified)) {
                    String[] md5s = calculateMD5(resources);
                    if(!Arrays.equals(md5, md5s)) {
                        refreshApplicationContext();
                        this.md5 = md5s;
                    }
                    // must refresh app context
                    this.lastModified = modifications;
                }
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
    }

    private AbstractRefreshableApplicationContext context;

    private ScheduledExecutorService executor;

    private List<String> resourcesToMonitor = new ArrayList<String>();

    private int refreshPeriod = 2;

    private boolean checkMD5 = true;

    private boolean autodetectResources;

    private boolean parallelStart = true;

    /**
     * {@inheritDoc}
     */
    public synchronized void setApplicationContext(ApplicationContext applicationContext) {
        if (applicationContext instanceof AbstractRefreshableApplicationContext) {
            this.context = (AbstractRefreshableApplicationContext) applicationContext;
        }
    }

    /**
     * {@inheritDoc}
     */
    public synchronized void destroy() throws Exception {
        if (executor != null) {
            executor.shutdown();
            executor = null;
        }
    }

    /**
     * {@inheritDoc}
     */
    public synchronized void afterPropertiesSet() throws Exception {
        if (DebugRuntimeFactory.ENABLED && context != null) {
            Runnable task = new Runnable() {
                public void run() {
                    try {
                        setupResourcesMonitoring();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            };
            if (this.parallelStart) {
                this.getExecutor().submit(task);
            } else {
                task.run();
            }
        }
    }

    /**
     * @throws IOException e
     */
    private void setupResourcesMonitoring() throws IOException {
        final Resource[] resources = monitoringResources();
        this.getExecutor().scheduleWithFixedDelay(
                new ApplicationContextRefresherTask(resources, lastModified(resources), calculateMD5(resources)),
                refreshPeriod, refreshPeriod, TimeUnit.SECONDS);
    }

    /**
     * Calcula o MD5 dos {@link Resource}
     * 
     * @param resources {@link Resource}
     * @return {@link String}
     * @throws IOException e
     */
    private String[] calculateMD5(Resource[] resources) throws IOException {
        if (!this.checkMD5) {
            return EMPTY_STRING;
        }
        String[] md5s = new String[resources.length];
        for (int i = 0; i < md5s.length; i++) {
            md5s[i] = MD5SumTool.md5Sum(resources[i].getFile());
        }
        return md5s;
    }

    /**
     * @return array de {@link Resource}
     */
    private Resource[] monitoringResources() {
        Set<Resource> resources = new HashSet<Resource>();
        if (this.autodetectResources) {
            // this may take a while...
            String[] beanNames = this.context.getBeanFactory().getBeanDefinitionNames();
            for (String beanName : beanNames) {
                BeanDefinition def = this.context.getBeanFactory().getBeanDefinition(beanName);
                if (def instanceof AbstractBeanDefinition) {
                    AbstractBeanDefinition beanDef = (AbstractBeanDefinition) def;
                    resources.add(beanDef.getResource());
                }
            }
        }
        for (String path : this.resourcesToMonitor) {
            resources.add(context.getResource(path));
        }
        Collection<Resource> set = filterFiles(resources);
        return set.toArray(new Resource[set.size()]);
    }

    /**
     * Filtra os {@link Resource} que sao {@link File}
     * 
     * @param resources {@link Collection} de {@link Resource}
     * @return {@link Collection}
     */
    private Collection<Resource> filterFiles(Collection<Resource> resources) {
        Iterator<Resource> iterator = resources.iterator();
        while (iterator.hasNext()) {
            Resource r = iterator.next();
            try {
                r.getFile();
            } catch (IOException e) {
                iterator.remove();
            }
        }
        return resources;
    }

    /**
     * @param resources array de {@link Resource}
     * @return array de long
     * @throws IOException e
     */
    private long[] lastModified(Resource[] resources) throws IOException {
        long[] times = new long[resources.length];
        for (int i = 0; i < times.length; i++) {
            File file = resources[i].getFile();
            times[i] = file.lastModified();
        }
        return times;
    }

    /**
     * @return the executor
     */
    private synchronized ScheduledExecutorService getExecutor() {
        if (executor == null) {
            executor = Executors.newScheduledThreadPool(1);
        }
        return executor;
    }

    /**
     * @param executor the executor to set
     */
    public void setExecutor(ScheduledExecutorService executor) {
        this.executor = executor;
    }

    /**
     * @param resoucesToMonitor the resoucesToMonitor to set
     */
    public void setResourcesToMonitor(List<String> resoucesToMonitor) {
        this.resourcesToMonitor = resoucesToMonitor;
    }

    /**
     * @param refreshPeriod the refreshPeriod to set
     */
    public void setRefreshPeriod(int refreshPeriod) {
        this.refreshPeriod = refreshPeriod;
    }

    /**
     * Refreshes the applicationContext
     */
    public void refreshApplicationContext() {
        context.refresh();
    }

    /**
     * @param checkMD5 the checkMD5 to set
     */
    public void setCheckMD5(boolean checkMD5) {
        this.checkMD5 = checkMD5;
    }

    /**
     * @param autodetectResources the autodetectResources to set
     */
    public void setAutodetectResources(boolean autodetectResources) {
        this.autodetectResources = autodetectResources;
    }

    /**
     * @param parallelStart the parallelStart to set
     */
    public void setParallelStart(boolean parallelStart) {
        this.parallelStart = parallelStart;
    }

}
